#region License Info

//Component of Cronos Package, http://www.codeplex.com/cronos
//Copyright (C) 2009 Anthony Brockwell

//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using ABMath.ModelFramework.Data;
using MathNet.Numerics.LinearAlgebra;
using PdfSharp.Drawing;

namespace ExtraControlLibrary
{
    public partial class TSPlotBox : PartitionedBlankGraph
    {
        #region Delegates

        public delegate void OnRegionSelect(object sender, RegionSelectArgs e);

        public delegate void OnScrolledEventHandler(object sender, EventArgs e);

        #endregion

        private readonly List<PlotBoxExtra> extraComponents;
        private SingleSeriesInfo allSeriesInfo;

        private Vector displayedXRange;
        private HScrollBar horizontalScroller;

        /// <summary>
        /// series[] is an array of lists of points.  Each point is represented
        /// by a 2-dimensional vector (x and y coordinates).
        /// </summary>
        private List<TimeSeries> series;

        private List<SingleSeriesInfo> seriesInfo;


        /// <summary>
        /// Constructor: creates blank (no data) plot.
        /// </summary>
        public TSPlotBox()
        {
            extraComponents = new List<PlotBoxExtra>();
            MultivariateActiveSeries = 0;
            MultivariateCommonScale = false; // use different scales to show qualitative features of all
            series = new List<TimeSeries>();
            allSeriesInfo = new SingleSeriesInfo();
            lineMode = true;
            InitializeComponent();
        }

        public int MultivariateActiveSeries { get; set; }
        public bool MultivariateCommonScale { get; set; }

        /// <summary>
        /// Vector used to allow for horizontally scrolling plots: set XRange as a vector (xmin,xmax).
        /// This is the range that is displayed (out of the entire available range as specified by
        /// XLimits or by the data if XLimits==null).
        /// </summary>
        public Vector XRange
        {
            get { return displayedXRange; }
        }

        public event OnRegionSelect RegionSelectEvent;

        protected virtual void OnRegionSelectEvent(RegionSelectArgs rsa)
        {
            if (RegionSelectEvent != null)
                RegionSelectEvent(this, rsa);
        }

        public event OnScrolledEventHandler OnScrolled = null;

        public int NumSeries()
        {
            if (series == null)
                return 0;
            return series.Count;
        }

        public double FirstPoint(int seriesindex)
        {
            if (NumSeries() <= seriesindex)
                throw new ArgumentException("Invalid series index.");
            if (series[seriesindex].Count == 0)
                throw new ApplicationException("Attempt to access empty time series.");
            return series[seriesindex][0];
        }

        public double LastPoint(int seriesindex)
        {
            if (NumSeries() <= seriesindex)
                throw new ArgumentException("Invalid series index.");
            int n = series[seriesindex].Count;
            if (n > 0)
                return series[seriesindex][n - 1];
            throw new ApplicationException("Attempt to access empty time series.");
        }

        /// <summary>
        /// Empties the plot box, readies it for new AppendToSeries calls.
        /// </summary>
        public void Clear(bool redraw)
        {
            ClearComponents();
            series = new List<TimeSeries>();
            allSeriesInfo = new SingleSeriesInfo();
            displayedXRange = null;
            if (redraw)
                Refresh();
        }

        public void AddComponent(PlotBoxExtra pe)
        {
            extraComponents.Add(pe);
        }

        public void ClearComponents()
        {
            extraComponents.Clear();
        }


        /// <summary>
        /// adds a TimeSeries object to the plot, and returns a handle
        /// </summary>
        /// <param name="newseries"></param>
        /// <returns></returns>
        public int AddSeries(TimeSeries newseries)
        {
            return AddSeries(newseries, true);
        }

        /// <summary>
        /// adds a TimeSeries object to the plot, and returns a handle, allows caller
        /// to determine whether or not to rescan
        /// </summary>
        /// <param name="newseries"></param>
        /// <returns></returns>
        public int AddSeries(TimeSeries newseries, bool rescan)
        {
            series.Add(newseries);
            if (rescan)
                ScanData();
            return series.Count - 1;
        }


        /// <summary>
        /// Appends the data point to the end of the plot and updates
        /// everything accordingly.
        /// </summary>
        public void AppendToSeries(DateTime timeStamp, double value, int seriesindex, bool force, bool redraw)
        {
            // Validity Checks
            if ((seriesindex < 0))
                throw new ApplicationException("Invalid series number used in AppendToSeries.");

            // Final validity check
            bool outOfOrder = false;
            int lastn = series[seriesindex].Count;
            if (lastn > 0)
            {
                DateTime lastxval = series[seriesindex].TimeStamp(lastn - 1);
                if (timeStamp < lastxval)
                    outOfOrder = true;
            }

            // data modification
            if (outOfOrder)
                series[seriesindex].InsertInMiddle(timeStamp, value, force);
            else 
                series[seriesindex].Add(timeStamp, value, force);

            // update results of ScanData (the quick recursive way)
            allSeriesInfo.Update(timeStamp, value, displayedXRange);
            allSeriesInfo.ComputeRecommendedUnits(displayedXRange, TimeMode, tickResolution);
            seriesInfo[seriesindex].Update(timeStamp, value, displayedXRange);
            seriesInfo[seriesindex].ComputeRecommendedUnits(displayedXRange, TimeMode, tickResolution);

            // update horizontal scroller if necessary
            if (horizontalScroller != null)
                UpdateHScrollerGraphics(); // update scroller graphical properties to match range with new endpoint

            // finally, refresh the display
            if (redraw)
                Refresh();
        }

        /// <summary>
        /// This is the callback for horizontalScroller (if it's not null).
        /// </summary>
        /// <param name="o"></param>
        /// <param name="se"></param>
        private void HorizontalMove(object o, ScrollEventArgs se)
        {
            if (NumSeries() == 0)
                return;

            // update xrange accordingly
            int newpos = se.NewValue,
                largechange = horizontalScroller.LargeChange;
            double max = horizontalScroller.Maximum;

            if (displayedXRange != null)
            {
                double x0 = newpos/max*(allSeriesInfo.xend - allSeriesInfo.xstart) + allSeriesInfo.xstart;
                double x1 = largechange/max*(allSeriesInfo.xend - allSeriesInfo.xstart) + x0;

                displayedXRange = new Vector(2);
                displayedXRange[0] = x0;
                displayedXRange[1] = x1;
            }

            Refresh();

            // finally, fire the onscrolled event
            if (OnScrolled != null)
                OnScrolled(this, EventArgs.Empty);
        }

        /// <summary>
        /// updates all graphical parameters of the horizontal scroller to reflect XRange as a fraction of displayed range
        /// </summary>
        private void UpdateHScrollerGraphics()
        {
            if (horizontalScroller == null)
                return;
            if (NumSeries() == 0)
                return;
            if ((displayedXRange == null) || (allSeriesInfo.xstart == allSeriesInfo.xend))
            {
                horizontalScroller.Hide();
                return;
            }
            horizontalScroller.Show();

            // first determine sub-indices within XRange
            double barwidth = (displayedXRange[1] - displayedXRange[0])/(allSeriesInfo.xend - allSeriesInfo.xstart);
            double startpos = (displayedXRange[0] - allSeriesInfo.xstart)/(allSeriesInfo.xend - allSeriesInfo.xstart);

            // now set properties of scroller
            horizontalScroller.Maximum = 1000; // just make it nice and fine
            horizontalScroller.Value = (int) (Math.Floor(startpos*1000));
            horizontalScroller.LargeChange = (int) (Math.Floor(barwidth*1000));
        }


        /// <summary>
        /// This function goes through, evaluates max and min in each coordinate,
        /// and finds some "nice" units to subdivide the ranges into.
        /// Results are stored in member variables.
        /// </summary>
        public void ScanData()
        {
            if (NumSeries() == 0)
                return;

            allSeriesInfo = new SingleSeriesInfo();
            seriesInfo = new List<SingleSeriesInfo>();
            for (int i = 0; i < NumSeries(); ++i)
                seriesInfo.Add(new SingleSeriesInfo());
            for (int i = 0; i < NumSeries(); ++i)
                for (int j = 0; j < series[i].Count; ++j)
                {
                    seriesInfo[i].Update(series[i].TimeStamp(j), series[i][j], displayedXRange);
                    allSeriesInfo.Update(series[i].TimeStamp(j), series[i][j], displayedXRange);
                }

            allSeriesInfo.ComputeRecommendedUnits(displayedXRange, TimeMode, tickResolution);
            for (int i = 0; i < NumSeries(); ++i)
                seriesInfo[i].ComputeRecommendedUnits(displayedXRange, TimeMode, tickResolution);
        }

        protected void PutPointMarker(Graphics g, Pen pen, int pixelX, int pixelY)
        {
            if (markerSize > 0)
                g.DrawRectangle(pen, pixelX - markerSize, pixelY - markerSize, markerSize*2, markerSize*2);
        }

        protected void PutPointMarker(XGraphics g, Pen pen, double pixelX, double pixelY)
        {
            if (markerSize > 0)
                g.DrawRectangle(pen, pixelX - markerSize, pixelY - markerSize, markerSize*2, markerSize*2);
        }


        protected void DoXAxisLabeling(Object graphicsObject, Font labelFont)
        {
            // sf.Alignment = StringAlignment.Center;
            double x0 = LeftXValue(), x1 = RightXValue();

            // First create the x-axis value labels
            var xaxisvals = new List<double>();
            List<string> xaxisvallabels;

            for (double tx = allSeriesInfo.xlabel_start; tx <= allSeriesInfo.xlabel_end; tx += allSeriesInfo.xunits)
                if ((tx >= x0) && (tx <= x1))
                    xaxisvals.Add(tx - (timeIsRelative ? x0 : 0));

            // Each label should be different, if not we need finer detail,
            // so we use a do while loop below.

            int detaillevel = 1;
            bool doubleup;
            int i;
            do
            {
                xaxisvallabels = new List<string>();
                doubleup = false;

                string formatstring = "{0:0.";
                for (i = 0; i < detaillevel; ++i)
                    formatstring += "0";
                formatstring += "e+0}";

                foreach (double xval in xaxisvals)
                {
                    string lbl;

                    if (TimeMode)
                    {
                        var tickval = (long) xval;
                        if (tickval < DateTime.MinValue.Ticks)
                            tickval = DateTime.MinValue.Ticks;
                        if (tickval > DateTime.MaxValue.Ticks)
                            tickval = DateTime.MaxValue.Ticks;

                        if (timeIsRelative)
                        {
                            var tspan = new TimeSpan(tickval);
                            lbl = tspan.ToString();
                        }
                        else
                        {
                            var dttemp = new DateTime(tickval);
                            switch (detaillevel)
                            {
                                case 1:
                                    lbl = dttemp.ToString("dd/MMM/yy");
                                    break;
                                case 2:
                                    lbl = dttemp.ToString("dd/MMM/yy-HH:mm");
                                    break;
                                case 3:
                                    lbl = dttemp.ToString("ddd-HH:mm");
                                    break;
                                case 4:
                                    lbl = dttemp.ToString("ddd-HH:mm:ss");
                                    break;
                                default:
                                    lbl = "";
                                    break;
                            }
                        }
                    }
                    else
                        lbl = String.Format(formatstring, xval);


                    if (xaxisvallabels.Contains(lbl))
                        doubleup = true;

                    xaxisvallabels.Add(lbl);
                }
                if (doubleup)
                {
                    // increase the fineness
                    ++detaillevel;

                    // if exceeded max., break out of loop
                    if (TimeMode && detaillevel > 4)
                        break;
                    if (detaillevel > 4)
                        break;
                }
            } while (doubleup);


            // Draw all the x-axis labels making sure text doesn't overlap
            DrawAllXLabels(graphicsObject, labelFont, xaxisvals, xaxisvallabels, xAxisLabel);
        }


        protected void DoYAxisLabeling(object graphicsObject, Font labelFont, double y0, double y1, double yunits)
        {
            using (var sf = new StringFormat())
            {
                sf.Alignment = StringAlignment.Center;

                // First create the value labels
                var yaxisvals = new List<double>();
                List<string> yaxisvallabels;

                for (double ty = y0; ty <= y1; ty += yunits)
                    yaxisvals.Add(ty);

                // Each label should be different, if not we need finer detail,
                // so we use a do while loop below.

                int detaillevel = 1;
                bool doubleup;
                int i;
                do
                {
                    yaxisvallabels = new List<string>();
                    doubleup = false;

                    // now build string out of y-value to appropriate level of detail
                    if (!TickMode)
                    {
                        string formatstring;
                        if (!TickMode)
                        {
                            formatstring = "{0:0.";
                            for (i = 0; i < detaillevel; ++i)
                                formatstring += "0";
                            formatstring += "}";

                            foreach (double yval in yaxisvals)
                            {
                                string lbl = String.Format(formatstring, yval);
                                if (yaxisvallabels.Contains(lbl))
                                    doubleup = true;
                                yaxisvallabels.Add(lbl);
                            }
                        }
                        else
                        {
                            formatstring = "{0:##.0";
                            for (i = 0; i < detaillevel; ++i)
                                formatstring += "0";
                            formatstring += "}";

                            foreach (double yval in yaxisvals)
                            {
                                double yvalInt = Math.Floor(yval);
                                double yvalFrac = yval - yvalInt;
                                string lbl = yvalInt + "'" + String.Format(formatstring, TickResolution*yvalFrac);
                                yaxisvallabels.Add(lbl);
                            }
                        }
                    }
                    else
                    {
                        foreach (double yval in yaxisvals)
                        {
                            var intpart = (int) Math.Floor(yval);
                            double ticknum = (yval - intpart)*TickResolution;
                            string lbl = intpart + "'" + ticknum;
                            if (yaxisvallabels.Contains(lbl))
                                doubleup = true;
                            yaxisvallabels.Add(lbl);
                        }
                    }

                    if (doubleup)
                    {
                        // increase the fineness
                        ++detaillevel;

                        // if exceeded max., give up and break out of loop
                        if (detaillevel > 5)
                            break;
                    }
                } while (doubleup);


                DrawAllYLabels(graphicsObject, labelFont, yaxisvals, yaxisvallabels, yAxisLabel, y0, y1);
            }
        }


        private int[] GetSeriesIndicesFor(int seriesindex, double plotX0, double plotX1)
        {
            int i0, i1;
            int numPoints = series[seriesindex].Count;
            var x0Ticks = new DateTime((long) plotX0);
            var x1Ticks = new DateTime((long) plotX1);

            // now get a bit closer: find greatest i0 such that series[seriesindex][i0][0]<x0
            int i00 = 0, i01 = numPoints - 1;
            if (series[seriesindex].TimeStamp(i00) < x0Ticks)
            {
                while (i01 - i00 > 1)
                {
                    int i02 = (i00 + i01)/2;
                    if (series[seriesindex].TimeStamp(i02) >= x0Ticks)
                        i01 = i02;
                    else
                        i00 = i02;
                }
                i0 = i00; // series[...][i00][0] is always < x0 by construction
            }
            else
                i0 = 0;

            // then find smallest i1 such that series[seriesindex][i1][0]>=x1
            i00 = 0;
            i01 = numPoints - 1;
            if (series[seriesindex].TimeStamp(i01) >= x1Ticks)
            {
                while (i01 - i00 > 1)
                {
                    int i02 = (i00 + i01)/2;
                    if (series[seriesindex].TimeStamp(i02) < x1Ticks)
                        i00 = i02;
                    else
                        i01 = i02;
                }
                i1 = i01 + 1;
            }
            else
                i1 = numPoints;

            return new[] {i0, i1};
        }

        public void RenderToXGraphics(XGraphics g, int titleFontSize, int labelFontSize)
        {
            // Now draw things.
            using (var titleFont = new Font("Arial", titleFontSize, FontStyle.Bold, GraphicsUnit.World))
            using (var labelFont = new Font("Arial", labelFontSize, FontStyle.Regular, GraphicsUnit.World))
            {
                DrawTitle(titleFont, g);

                // Return if nothing to plot
                if ((series == null) || (allSeriesInfo.xRange() == 0) || (allSeriesInfo.yRange() == 0))
                    return;

                double plotX0 = LeftXValue();
                double plotX1 = RightXValue();

                double plotY0 = !MultivariateCommonScale
                                    ? seriesInfo[MultivariateActiveSeries].boundingMinY
                                    : allSeriesInfo.boundingMinY;
                double plotY1 = !MultivariateCommonScale
                                    ? seriesInfo[MultivariateActiveSeries].boundingMaxY
                                    : allSeriesInfo.boundingMaxY;
                if (plotY0 == plotY1)
                {
                    plotY0 = -seriesInfo[MultivariateActiveSeries].yunits;
                    plotY1 = seriesInfo[MultivariateActiveSeries].yunits;
                }
                double plotYUnits = !MultivariateCommonScale
                                        ? seriesInfo[MultivariateActiveSeries].yunits
                                        : allSeriesInfo.yunits;
                // then all the x-axis labeling
                DoXAxisLabeling(g, labelFont);

                // Draw the y-axis values
                DoYAxisLabeling(g, labelFont, plotY0, plotY1, plotYUnits);

                // Now restrict drawing range to graph area
                var clipregion = new Rectangle(GraphRectangle.Left + 1, GraphRectangle.Top + 1,
                                               GraphRectangle.Width, GraphRectangle.Height);

                // Then put in the gray background lines
                for (double tx = allSeriesInfo.xlabel_start;
                     tx <= allSeriesInfo.xlabel_end;
                     tx += allSeriesInfo.xunits)
                {
                    float xpix = MaptoXPosition(tx);
                    if (xpix >= clipregion.Left && xpix <= clipregion.Right)
                        g.DrawLine(Pens.LightGray, xpix, GraphRectangle.Top, xpix, GraphRectangle.Bottom);
                }

                for (double ty = plotY0;
                     ty <= plotY1;
                     ty += plotYUnits)
                {
                    float ypix = (float)(-(ty - plotY0) / (plotY1 - plotY0) * GraphRectangle.Height + GraphRectangle.Bottom);
                    if (ypix >= clipregion.Top && ypix <= clipregion.Bottom)
                        g.DrawLine(Pens.LightGray, GraphRectangle.Left, ypix, GraphRectangle.Right, ypix);
                }

                // And draw frame
                DrawGraphFrame(g);

                var penlist = new Pen[5];
                penlist[0] = new Pen(Brushes.Blue, (float) 0.5);
                penlist[1] = new Pen(Brushes.Red, (float) 0.5);
                penlist[2] = new Pen(Brushes.Green, (float) 0.5);
                penlist[3] = new Pen(Brushes.Black, (float) 0.5);
                penlist[4] = new Pen(Brushes.LightGray, (float) 0.5);

                // DRAW THE DATA.

                // But make sure it's in a sequence that draws the active series last so 
                // it doesn't get covered up by others.
                var seriesSequence = new int[NumSeries()];
                for (int i = 0; i < NumSeries(); ++i)
                    seriesSequence[i] = i;
                seriesSequence[NumSeries() - 1] = MultivariateActiveSeries;
                seriesSequence[MultivariateActiveSeries] = NumSeries() - 1;

                // Now we have a sequence, start drawing.
                foreach (int seriesIndex in seriesSequence)
                    if (series[seriesIndex].Count > 0)
                    {
                        Pen temppen = seriesIndex == MultivariateActiveSeries ? penlist[0] : penlist[penlist.Length - 1];

                        if (!MultivariateCommonScale)
                        {
                            plotY0 = seriesInfo[seriesIndex].boundingMinY;
                            plotY1 = seriesInfo[seriesIndex].boundingMaxY;
                            if (plotY0 == plotY1)
                            {
                                plotY0 = -seriesInfo[seriesIndex].yunits;
                                plotY1 = seriesInfo[seriesIndex].yunits;
                            }
                        }

                        // now get indices of points falling in the appropriate x-range
                        int[] indexLimits = GetSeriesIndicesFor(seriesIndex, plotX0, plotX1);
                        int i0 = indexLimits[0], i1 = indexLimits[1];
                        int numSubPoints = i1 - i0 + 1;
                        double[] xpixels = new double[numSubPoints], ypixels = new double[numSubPoints];
                        var drawIndices = new List<int>(2000);

                        // now compute all the x and y pixel coordinates
                        for (int i = i0; i < i1; ++i)
                            xpixels[i - i0] =
                                ((series[seriesIndex].TimeStamp(i).Ticks - plotX0)/(plotX1 - plotX0)*
                                 GraphRectangle.Width) + GraphRectangle.Left;

                        double curXPixel = xpixels[0];
                        double firstPoint = series[seriesIndex][i0];
                        double pixelMinY = firstPoint;
                        double pixelMaxY = pixelMinY;
                        int argMin = i0;
                        int argMax = i0;
                        ypixels[0] = (-(firstPoint - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                     GraphRectangle.Bottom;

                        for (int i = i0 + 1; i < i1; ++i)
                        {
                            double yval = series[seriesIndex][i];
                            ypixels[i - i0] = (-(yval - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                              GraphRectangle.Bottom;
                            if (xpixels[i - i0] != curXPixel)
                            {
                                // it's a new pixel, so just summarize the last block and begin the new
                                drawIndices.Add(argMin - i0);
                                if (argMax != argMin)
                                    drawIndices.Add(argMax - i0);
                                argMin = i;
                                argMax = i;
                                curXPixel = xpixels[i - i0];
                                pixelMinY = yval;
                                pixelMaxY = yval;
                            }
                            else
                            {
                                // scan for min and max within the pixel
                                if (yval > pixelMaxY)
                                {
                                    pixelMaxY = yval;
                                    argMax = i;
                                }
                                if (yval < pixelMinY)
                                {
                                    pixelMinY = yval;
                                    argMin = i;
                                }
                            }
                        }
                        drawIndices.Add(argMin - i0);
                        if (argMax != argMin)
                            drawIndices.Add(argMax - i0);

                        // draw the lines
                        if (lineMode)
                        {
                            for (int j = 0; j < drawIndices.Count - 1; ++j)
                            {
                                int idx1 = drawIndices[j];
                                int idx2 = drawIndices[j + 1];
                                double y1 = ypixels[idx1];
                                double y2 = ypixels[idx2];
                                if ((y1 < 0) || (y1 > 2500))
                                    y1 = 0;
                                if ((y2 < 0) || (y2 > 2500))
                                    y2 = 0;
                                if (StepMode)
                                {
                                    g.DrawLine(temppen, xpixels[idx1], y1, xpixels[idx2], y1);
                                    g.DrawLine(temppen, xpixels[idx2], y1, xpixels[idx2], y2);
                                }
                                else
                                    g.DrawLine(temppen, xpixels[idx1], ypixels[idx1], xpixels[idx2],
                                               ypixels[idx2]);
                            }
                        }

                        // we put down the point markers in any case
                        for (int j = 0; j < drawIndices.Count; ++j)
                        {
                            int idx1 = drawIndices[j];
                            double y1 = ypixels[idx1];
                            PutPointMarker(g, temppen, xpixels[idx1], y1);
                        }

                        // if necessary, extend the line to the right
                        if (extendRight)
                        {
                            int lastIdx = drawIndices[drawIndices.Count - 1];
                            double lastY = ypixels[lastIdx];
                            double lastX = xpixels[lastIdx];
                            g.DrawLine(temppen, lastX, lastY, GraphRectangle.Left + GraphRectangle.Width, lastY);
                        }
                    }
            }
        }

        public void RenderToGraphics(Graphics g, int titleFontSize, int labelFontSize)
        {
            // Now draw things.
            using (var titleFont = new Font("Arial", titleFontSize, FontStyle.Bold))
            using (var labelFont = new Font("Arial", labelFontSize))
            {
                DrawTitle(titleFont, g);
                ClearGraphArea(g);

                // Return if nothing to plot
                if ((series == null) || (allSeriesInfo.xRange() == 0) || (allSeriesInfo.yRange() == 0))
                    return;

                double plotX0 = LeftXValue();
                double plotX1 = RightXValue();

                double plotY0 = !MultivariateCommonScale
                                    ? seriesInfo[MultivariateActiveSeries].boundingMinY
                                    : allSeriesInfo.boundingMinY;
                double plotY1 = !MultivariateCommonScale
                                    ? seriesInfo[MultivariateActiveSeries].boundingMaxY
                                    : allSeriesInfo.boundingMaxY;
                if (plotY0 == plotY1)
                {
                    plotY0 = -seriesInfo[MultivariateActiveSeries].yunits;
                    plotY1 = seriesInfo[MultivariateActiveSeries].yunits;
                }
                double plotYUnits = !MultivariateCommonScale
                                        ? seriesInfo[MultivariateActiveSeries].yunits
                                        : allSeriesInfo.yunits;

                // then all the x-axis labeling
                DoXAxisLabeling(g, labelFont);

                // Draw the y-axis values
                DoYAxisLabeling(g, labelFont, plotY0, plotY1, plotYUnits);

                // Now restrict drawing range to graph area
                var clipregion =
                    new Rectangle(GraphRectangle.Left + 1, GraphRectangle.Top + 1, GraphRectangle.Width,
                                  GraphRectangle.Height);
                using (var region = new Region(clipregion))
                {
                    g.Clip = region;

                    // Now any extras
                    foreach (PlotBoxExtra pextra in extraComponents)
                        pextra.Render(this, g);


                    // Then put in the gray background lines
                    for (double tx = allSeriesInfo.xlabel_start;
                         tx <= allSeriesInfo.xlabel_end;
                         tx += allSeriesInfo.xunits)
                    {
                        int xpix = MaptoXPixel(tx);
                        g.DrawLine(Pens.LightGray, xpix, GraphRectangle.Top, xpix, GraphRectangle.Bottom);
                    }

                    for (double ty = plotY0; ty <= plotY1; ty += plotYUnits)
                    {
                        int ypix = (int)(-(ty - plotY0) / (plotY1 - plotY0) * GraphRectangle.Height) + GraphRectangle.Bottom;
                        g.DrawLine(Pens.LightGray, GraphRectangle.Left, ypix, GraphRectangle.Right, ypix);
                    }

                    // and draw frame
                    DrawGraphFrame(g);

                    var penlist = new Pen[5];
                    penlist[0] = Pens.Blue;
                    penlist[1] = Pens.Red;
                    penlist[2] = Pens.Green;
                    penlist[3] = Pens.Black;
                    penlist[4] = Pens.LightGray; // last one is background in shadowmode

                    // DRAW THE DATA.

                    // But make sure it's in a sequence that draws the active series last so 
                    // it doesn't get covered up by others.
                    var seriesSequence = new int[NumSeries()];
                    for (int i = 0; i < NumSeries(); ++i)
                        seriesSequence[i] = i;
                    seriesSequence[NumSeries() - 1] = MultivariateActiveSeries;
                    seriesSequence[MultivariateActiveSeries] = NumSeries() - 1;

                    // Now we have a sequence, start drawing.
                    foreach (int seriesIndex in seriesSequence)
                        if (series[seriesIndex].Count > 0)
                        {
                            Pen temppen = seriesIndex == MultivariateActiveSeries
                                              ? penlist[0]
                                              : penlist[penlist.Length - 1];

                            if (!MultivariateCommonScale)
                            {
                                plotY0 = seriesInfo[seriesIndex].boundingMinY;
                                plotY1 = seriesInfo[seriesIndex].boundingMaxY;
                                if (plotY0 == plotY1)
                                {
                                    plotY0 = -seriesInfo[seriesIndex].yunits;
                                    plotY1 = seriesInfo[seriesIndex].yunits;
                                }
                            }

                            // now get indices of points falling in the appropriate x-range
                            int[] indexLimits = GetSeriesIndicesFor(seriesIndex, plotX0, plotX1);
                            int i0 = indexLimits[0], i1 = indexLimits[1];
                            int numSubPoints = i1 - i0 + 1;
                            int[] xpixels = new int[numSubPoints], ypixels = new int[numSubPoints];
                            var drawIndices = new List<int>(2000);

                            // now compute all the x and y pixel coordinates
                            for (int i = i0; i < i1; ++i)
                                xpixels[i - i0] =
                                    (int)
                                    ((series[seriesIndex].TimeStamp(i).Ticks - plotX0)/(plotX1 - plotX0)*
                                     GraphRectangle.Width) + GraphRectangle.Left;

                            int curXPixel = xpixels[0];
                            double firstPoint = series[seriesIndex][i0];
                            double pixelMinY = firstPoint;
                            double pixelMaxY = pixelMinY;
                            int argMin = i0;
                            int argMax = i0;
                            ypixels[0] = (int) (-(firstPoint - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                         GraphRectangle.Bottom;

                            for (int i = i0 + 1; i < i1; ++i)
                            {
                                double yval = series[seriesIndex][i];
                                ypixels[i - i0] = (int) (-(yval - plotY0)/(plotY1 - plotY0)*GraphRectangle.Height) +
                                                  GraphRectangle.Bottom;
                                if (xpixels[i - i0] != curXPixel)
                                {
                                    // it's a new pixel, so just summarize the last block and begin the new
                                    drawIndices.Add(argMin - i0);
                                    if (argMax != argMin)
                                        drawIndices.Add(argMax - i0);
                                    argMin = i;
                                    argMax = i;
                                    curXPixel = xpixels[i - i0];
                                    pixelMinY = yval;
                                    pixelMaxY = yval;
                                }
                                else
                                {
                                    // scan for min and max within the pixel
                                    if (yval > pixelMaxY)
                                    {
                                        pixelMaxY = yval;
                                        argMax = i;
                                    }
                                    if (yval < pixelMinY)
                                    {
                                        pixelMinY = yval;
                                        argMin = i;
                                    }
                                }
                            }
                            drawIndices.Add(argMin - i0);
                            if (argMax != argMin)
                                drawIndices.Add(argMax - i0);

                            // draw the lines
                            if (lineMode)
                                for (int j = 0; j < drawIndices.Count - 1; ++j)
                                {
                                    int idx1 = drawIndices[j];
                                    int idx2 = drawIndices[j + 1];
                                    int y1 = ypixels[idx1];
                                    int y2 = ypixels[idx2];
                                    if ((y1 < 0) || (y1 > 2500))
                                        y1 = 0;
                                    if ((y2 < 0) || (y2 > 2500))
                                        y2 = 0;
                                    if (StepMode)
                                    {
                                        g.DrawLine(temppen, xpixels[idx1], y1, xpixels[idx2], y1);
                                        g.DrawLine(temppen, xpixels[idx2], y1, xpixels[idx2], y2);
                                    }
                                    else
                                        g.DrawLine(temppen, xpixels[idx1], ypixels[idx1], xpixels[idx2],
                                                   ypixels[idx2]);
                                }

                            // we put down the point markers in any case
                            for (int j = 0; j < drawIndices.Count; ++j)
                            {
                                int idx1 = drawIndices[j];
                                int y1 = ypixels[idx1];
                                PutPointMarker(g, temppen, xpixels[idx1], y1);
                            }

                            // if necessary, extend the line to the right
                            if (extendRight)
                            {
                                int lastIdx = drawIndices[drawIndices.Count - 1];
                                int lastY = ypixels[lastIdx];
                                int lastX = xpixels[lastIdx];
                                g.DrawLine(temppen, lastX, lastY, GraphRectangle.Left + GraphRectangle.Width, lastY);
                            }
                        }
                }
            }
        }

        protected override void OnPaint(PaintEventArgs pe)
        {
            // don't over-paint a wire frame if there's one there
            if (frameDrawn)
                if (anchored)
                    return;

            Graphics g = pe.Graphics;

            // Preliminary processing: features of data we need to plot have already been set by ScanData
            // ScanData is called by set method for XYMatrix.

            // Now we partition into sub-boxes of this box for title, x-labels, y-labels, and graph
            CreatePartition(new Rectangle(0, 0, Size.Width, Size.Height));
            RenderToGraphics(g, TitleFontSize, LabelFontSize);
        }

        private void PlotBox_Resize(object sender, EventArgs e)
        {
            // ensure that contents are redrawn in new scale
            Refresh();
        }

        private void PlotBox_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.PageDown)
            {
                ++MultivariateActiveSeries;
                MultivariateActiveSeries %= NumSeries();
                Refresh();
            }
            if (e.KeyCode == Keys.PageUp)
            {
                --MultivariateActiveSeries;
                if (MultivariateActiveSeries < 0)
                    MultivariateActiveSeries += NumSeries();
                Refresh();
            }
        }

        #region Region Selecting Stuff

        #region Delegates

        public delegate void RegionSelectEventHandler(object source, double x1, double y1, double x2, double y2);

        #endregion

        private Point anchor;
        private bool anchored;
        private Point final;
        private bool frameDrawn;
        private Point newCorner;

        // Convert and normalize the points and draw the reversible frame.
        private void DrawReversibleRectangle(Point p1, Point p2)
        {
            var rc = new Rectangle();

            // Convert the points to screen coordinates.
            p1 = PointToScreen(p1);
            p2 = PointToScreen(p2);

            // Normalize the rectangle.
            if (p1.X < p2.X)
            {
                rc.X = p1.X;
                rc.Width = p2.X - p1.X;
            }
            else
            {
                rc.X = p2.X;
                rc.Width = p1.X - p2.X;
            }
            if (p1.Y < p2.Y)
            {
                rc.Y = p1.Y;
                rc.Height = p2.Y - p1.Y;
            }
            else
            {
                rc.Y = p2.Y;
                rc.Height = p1.Y - p2.Y;
            }

            // Draw the reversible frame.
            ControlPaint.DrawReversibleFrame(rc, Color.Black, FrameStyle.Dashed); // , Color.Blue, FrameStyle.Dashed);
        }


        private void PlotBox_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) // region select is done with left-mouse button
                return;

            anchored = true;
            anchor = new Point(e.X, e.Y);
            frameDrawn = false;
        }

        private void PlotBox_MouseMove(object sender, MouseEventArgs e)
        {
            var newPoint = new Point(e.X, e.Y);
            if (newPoint != newCorner)
                if (anchored)
                {
                    // If we have drawn previously, draw again in
                    // that spot to remove the lines.
                    if (frameDrawn)
                        DrawReversibleRectangle(anchor, newCorner);

                    // Update last point.
                    newCorner = newPoint;

                    // Draw new lines.
                    DrawReversibleRectangle(anchor, newCorner);
                    frameDrawn = true;
                }
        }

        private void PlotBox_MouseUp(object sender, MouseEventArgs e)
        {
            if (!anchored)
                return;

            final = new Point(e.X, e.Y);

            anchored = false;
            if (frameDrawn)
            {
                DrawReversibleRectangle(anchor, newCorner); // erase it
                frameDrawn = false;
            }

            if (final != anchor)
            {
                // translate pixel coordinates to other coordinates
                double x1 = XValueToPixel(anchor.X);
                double y1 = YValueToPixel(anchor.Y);
                double x2 = XValueToPixel(final.X);
                double y2 = YValueToPixel(final.Y);

                var ra = new RegionSelectArgs();
                ra.x1 = x1;
                ra.y1 = y1;
                ra.x2 = x2;
                ra.y2 = y2;
                OnRegionSelectEvent(ra);
            }
        }

        #endregion

        #region Property get/set methods

        private bool extendRight;
        private bool lineMode;
        private int markerSize = 2;
        private bool tickMode;
        private int tickResolution;

        private bool timeIsRelative;
        private bool timeMode = true;

        private string xAxisLabel = "";
        private string yAxisLabel = "";

        /// <summary>
        /// if true, then times are not labelled absolutely, instead they are all
        /// relative to the minimum time
        /// </summary>
        public bool TimeIsRelative
        {
            get { return timeIsRelative; }
            set { timeIsRelative = value; }
        }

        /// <summary>
        /// True if latest point should be extended in a line to the right edge of the plot.
        /// - useful for real-time display of values.
        /// </summary>
        public bool ExtendRight
        {
            get { return extendRight; }
            set { extendRight = value; }
        }


        /// This label goes under the x-axis.
        /// </summary>
        public string XAxisLabel
        {
            get { return xAxisLabel; }
            set
            {
                xAxisLabel = value;
                Refresh();
            }
        }

        /// <summary>
        /// <summary>
        /// This label goes sideways to the left of the y-axis.
        /// </summary>
        public string YAxisLabel
        {
            get { return yAxisLabel; }
            set
            {
                yAxisLabel = value;
                Refresh();
            }
        }


        /// <summary>
        /// True if x-axis should be converted to date/times (values are tick values),  false otherwise.
        /// </summary>
        public bool TimeMode
        {
            get { return timeMode; }
            set
            {
                if (timeMode != value)
                {
                    timeMode = value;
                    Refresh();
                }
            }
        }


        /// <summary>
        /// true if points should be connected with lines.  false for isolated points. 
        /// </summary>
        public bool LineMode
        {
            get { return lineMode; }
            set
            {
                lineMode = value;
                Refresh();
            }
        }

        public HScrollBar HorizontalScroller
        {
            get { return horizontalScroller; }
            set
            {
                horizontalScroller = value;
                // adjust callback
                if (horizontalScroller != null)
                    horizontalScroller.Scroll += HorizontalMove;
            }
        }

        /// <summary>
        /// Size of boxes put at each point
        /// </summary>
        public int MarkerSize
        {
            get { return markerSize; }
            set
            {
                markerSize = value;
                Refresh();
            }
        }

        /// <summary>
        /// true if plot should step to new values instead of connecting lines
        /// </summary>
        public bool StepMode { get; set; }

        public bool TickMode
        {
            get { return tickMode; }
            set
            {
                if (tickMode != value)
                {
                    tickMode = value;
                    Refresh();
                }
            }
        }

        /// <summary>
        /// If this is non-zero, then the y-axis is in tick units (1/32nds, etc. would be used if tickResolution==32)
        /// </summary>
        public int TickResolution
        {
            get { return tickResolution; }
            set
            {
                if (tickResolution != value)
                {
                    tickResolution = value;
                    Refresh();
                }
            }
        }

        public void SetXRange(Vector range)
        {
            displayedXRange = range;
            ScanData();
            UpdateHScrollerGraphics();
            Refresh();
        }

        #endregion

        #region Stuff for Ticker Mode

        private Timer tickerTimer;
        private long tickInterval;

        public void BeginTicker(TimeSpan tickInterval, int millisecondsForUpdate)
        {
            tickerTimer = new Timer();
            tickerTimer.Tick += TickEvent;
            tickerTimer.Interval = millisecondsForUpdate;
            tickerTimer.Enabled = true;
            this.tickInterval = tickInterval.Ticks;
        }

        private void TickEvent(object sender, EventArgs e)
        {
            long t1 = DateTime.Now.Ticks;
            long t0 = t1 - tickInterval;
            SetXRange(new Vector(new[] {(double) t0, t1}));

            // now trim so we don't run out of memory
            foreach (TimeSeries ts in series)
                ts.DeleteBefore(new DateTime(t0), true);
        }

        public void EndTicker()
        {
            tickerTimer.Enabled = false;
        }

        #endregion

        #region Methods specifying values corresponding to edges of plot

        protected override double LeftXValue()
        {
            if (displayedXRange != null)
                return displayedXRange[0];
            return allSeriesInfo.xstart;
        }

        protected override double RightXValue()
        {
            if (displayedXRange != null)
                return displayedXRange[1];
            if (allSeriesInfo != null)
                return allSeriesInfo.xend;
            return 0;
        }

        protected override double MinYValue()
        {
            return allSeriesInfo.boundingMinY;
        }

        protected override double MaxYValue()
        {
            return allSeriesInfo.boundingMaxY;
        }

        #endregion

        #region Nested type: RegionSelectArgs

        public class RegionSelectArgs : EventArgs
        {
            public double x1;
            public double x2;
            public double y1;
            public double y2;
        }

        #endregion

        #region Nested type: SingleSeriesInfo

        /// <summary>
        /// SingleSeriesInfo keeps track of x-max, x-min, y-max, y-min, 
        /// as well as the x and y bounds used in the graph.
        /// </summary>
        private class SingleSeriesInfo
        {
            public double boundingMaxY;
            public double boundingMinY;
            private bool isempty = true;
            public double xend; // values used as start and end (typically wider range than max and min)
            public double xlabel_end; // first and last value at which to put axis markers
            public double xlabel_start; // first and last value at which to put axis markers

            public double xmax, xmin; // actual max and min values
            public double xstart; // values used as start and end (typically wider range than max and min)
            public double xunits; // unit = gap between axis markers
            public double yend;

            public double ymax, ymin;
            public double ystart;
            public double yunits;

            public double xRange()
            {
                return xmax - xmin;
            }

            public double yRange()
            {
                return ymax - ymin;
            }

            public void Update(DateTime timestamp, double value, Vector displayedXRange)
            {
                if (displayedXRange != null)
                    if (timestamp.Ticks < displayedXRange[0] || timestamp.Ticks > displayedXRange[1])
                        return;

                if (isempty)
                {
                    xmax = xmin = timestamp.Ticks;
                    ymax = ymin = value;
                    isempty = false;
                }
                else
                {
                    if (timestamp.Ticks > xmax)
                        xmax = timestamp.Ticks;
                    if (timestamp.Ticks < xmin)
                        xmin = timestamp.Ticks;
                    if (value > ymax)
                        ymax = value;
                    if (value < ymin)
                        ymin = value;
                }
            }

            public void ComputeRecommendedUnits(Vector displayedXRange, bool TimeMode,
                                                int tickResolution)
            {
                bool tickMode = tickResolution != 0;

                // first x-axis
                double x0, x1;
                if (displayedXRange == null)
                {
                    x0 = xmin;
                    x1 = xmax;
                }
                else
                {
                    x0 = displayedXRange[0];
                    x1 = displayedXRange[1];
                }
                xunits = GetNiceUnitForRange(x0, x1, 1.0, TimeMode);

                if (TimeMode) // realign to start at the beginning of a "nice" time unit
                {
                    double basedatetime = new DateTime(1900, 1, 1, 0, 0, 0).Ticks;
                    xlabel_start =
                        Math.Floor((x0 - basedatetime)/xunits)*xunits + basedatetime;
                    xlabel_end =
                        Math.Ceiling((x1 - basedatetime)/xunits)*xunits + basedatetime;
                    xstart = xmin;
                    xend = xmax;
                }
                else
                {
                    xlabel_start = Math.Floor(x0/xunits)*xunits;
                    xlabel_end = Math.Ceiling(x1/xunits)*xunits;
                    xstart = Math.Floor(xmin/xunits)*xunits;
                    xend = Math.Ceiling(xmax/xunits)*xunits;
                }

                // then y-axis
                yunits = !tickMode
                             ? GetNiceUnitForRange(ymin, ymax, 1.0, false)
                             : GetNiceUnitForRange(ymin, ymax, 1.0/tickResolution, false);
                ystart = Math.Floor(ymin/yunits)*yunits;
                yend = Math.Ceiling(ymax/yunits)*yunits;
                boundingMinY = ystart;
                boundingMaxY = yend;
            }
        }

        #endregion
    }
}
